import * as THREE from "three";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";

import * as CANNON from "cannon-es"
import CannonDebugger from 'cannon-es-debugger'

import tb from "./assets/kitchen_table.glb?url"
import ck from "./assets/cookie.glb?url"
import bg from "./assets/bg.jpg?url"
import hit from "./assets/hit.mp3?url"

const images = import.meta.glob(`./assets/textures/*.png`, { eager: true });
const imageUrls = Object.values(images).map((mod) => mod.default);

const hitSound = new Audio(hit)
const playHitSound = (collision) => {
  const impactStrength = collision.contact.getImpactVelocityAlongNormal()
  if (impactStrength > 1) {
    hitSound.volume = Math.max(0, Math.min(impactStrength * .25, 1));
    hitSound.currentTime = 0
    hitSound.play()
  }
}

class Sketch {
  constructor({ background = "#FFFFFF", el = document.body, evts }) {
    this.width = window.innerWidth;
    this.height = window.innerHeight;
    this.evts = evts;
    this.camera = new THREE.PerspectiveCamera(
      30,
      window.innerWidth / window.innerHeight,
      0.01,
      1000
    );

    this.scene = new THREE.Scene();
    this.renderer = new THREE.WebGLRenderer({ antialias: true });
    this.renderer.setSize(this.width, this.height);
    this.renderer.setClearColor(background, 1.0);
    this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
    this.renderer.useLegacyLights = false;
    this.renderer.outputColorSpace = THREE.SRGBColorSpace;
    this.renderer.toneMapping = THREE.CineonToneMapping;
    this.renderer.toneMappingExposure = 3.6;
    this.renderer.gammaFactor = 3;
    this.renderer.shadowMap.enabled = true;
    this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;

    el.appendChild(this.renderer.domElement);
    this.isPlaying = false;

    this.loadingManager = new THREE.LoadingManager();
    this.textureLoader = new THREE.TextureLoader(this.loadingManager);
    this.gltfLoader = new GLTFLoader(this.loadingManager);
    // const dracoLoader = new DRACOLoader();
    // dracoLoader.setDecoderPath("/draco/")
    // this.gltfLoader.setDRACOLoader(dracoLoader);

    this.camera.position.set(
      0.1,
      .95,
      1.35
    )

    this.mouse = new THREE.Vector2();
    this.raycaster = new THREE.Raycaster();

    this.changeControls = false;
    this.cookiesItems = []
    this.textures = []

    this.init();
    return this;
  }
  async init() {
    this.loadTexture();
    this.loadCookieModel();
    this.addBackground();
    this.addLight();
    this.addPlane()
    this.initReset();
    this.initCannon();

    this.loadingManager.onProgress = ((url, num, total) => {
      this.evts && this.evts.onProgress(~~(num / total * 100))
    })

    this.loadingManager.onLoad = () => {
      this.evts && this.evts.onLoad()
      this.createCookies()
      this.mouseEffects();
      this.setControls();
      this.play();
    }
  }
  loadTexture() {
    imageUrls.forEach(url=>{
      this.textures.push(this.textureLoader.load(url))
    })
  }
  setControls() {
    this.controls = new OrbitControls(this.camera, this.renderer.domElement);
    this.controls.minPolarAngle = Math.PI / 3 * .2;
    this.controls.maxPolarAngle = Math.PI / 3;
    this.controls.maxDistance = 120;
    this.controls.maxDistance = 2
    this.controls.addEventListener('change', () => {
      this.changeControls = true;
    })
  }
  initCannon() {
    this.world = new CANNON.World({
      gravity: new CANNON.Vec3(0, -9.82 * .18, 0), // m/s²
    })

    this.world.allowSleep = true

    let groundShape = new CANNON.Plane();
    let groundBody = new CANNON.Body({ mass: 0 });
    groundBody.addShape(groundShape);
    groundBody.position.y = -0.8
    groundBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2);
    this.world.addBody(groundBody);

    let tableBody = new CANNON.Body({
      mass: 0,
    });
    const tableShape = new CANNON.Box(new CANNON.Vec3(.88, .01, .58))
    tableBody.addShape(tableShape);
    tableBody.position.set(-.07, 0, 0);
    tableBody.type = CANNON.Body.STATIC;
    this.world.addBody(tableBody)

    this.cannonDebugger = CannonDebugger(this.scene, this.world, {
      onInit(body, mesh) {
        mesh.visible = true
        console.log(body);
      },
    })
  }
  mouseEffects() {
    this.handlePointerDown = this.pointerDown.bind(this)
    this.handlePointerMove = this.pointerMove.bind(this);
    this.handlePointerUp = this.pointerUp.bind(this);
    this.renderer?.domElement.addEventListener("pointerdown", this.handlePointerDown, false);
    this.renderer?.domElement.addEventListener("pointermove", this.handlePointerMove, false);
    this.renderer?.domElement.addEventListener("pointerup", this.handlePointerUp, false);
  }

  pointerDown(event) {
    this.changeControls = false;
  }
  pointerMove(event) {}
  pointerUp(event) {
    if (this.changeControls) return;
    this.mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
    this.mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;

    this.raycaster.setFromCamera(this.mouse, this.camera);
    const intersects = this.raycaster.intersectObjects(this.scene.children);

    if (intersects.length > 0) {
      const point = intersects[0].point;
      this.createCookies(new THREE.Vector3(point.x, 1, point.z))
    }
  }
  updatePhysics(dt) {
    this.world.fixedStep()
    // this.cannonDebugger.update()
    if (this.cookiesItems.length) {
      this.cookiesItems.forEach(({ cookies, body }) => {
        cookies.position.copy(body.position)
        cookies.quaternion.copy(body.quaternion)
      })
    }
  }
  addBackground() {
    const { scene } = this;
    let envMap = this.textureLoader.load(bg);
    envMap.colorSpace = THREE.SRGBColorSpace;
    envMap.mapping = THREE.EquirectangularRefractionMapping;
    scene.environment = envMap;
    scene.background = envMap;
  }
  addPlane() {
    this.gltfLoader.load(tb, (gltf) => {
      let table = gltf.scene;
      table.scale.setScalar(.1)
      table.rotation.y = Math.PI / 2
      table.position.y = -0.912
      table.traverse(mesh => {
        if (mesh.isMesh) {
          mesh.receiveShadow = true;
        }
      })
      this.scene.add(table)
    })
  }
  addLight() {
    let light = new THREE.AmbientLight(0xFFFFFF, .5);
    this.scene.add(light);
    let dlight = new THREE.DirectionalLight(0xFFFFFF, 2);
    dlight.position.set(0, 5, 0)
    dlight.castShadow = true;
    dlight.shadow.bias = -0.0001;
    dlight.shadow.mapSize.width = 512 * 2;
    dlight.shadow.mapSize.height = 512 * 2;
    this.scene.add(dlight);
  }
  initReset() {
    this.handleReset = this.reset.bind(this);
    window.addEventListener("resize", this.handleReset);
  }
  reset() {
    this.width = window.innerWidth;
    this.height = window.innerHeight;
    this.camera.aspect = this.width / this.height;
    this.camera.updateProjectionMatrix();
    this.renderer.setSize(this.width, this.height);
    this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
  }
  destroy() {
    this.isPlaying = false;
    window.removeEventListener("resize", this.handleReset);
    this.renderer?.domElement.removeEventListener("pointerdown", this.handlePointerDown, false);
    this.renderer?.domElement.removeEventListener("pointermove", this.handlePointerMove, false);
    this.renderer?.domElement.removeEventListener("pointerup", this.handlePointerUp, false);
    this.controls?.dispose();
    this.renderer?.dispose();
    this.renderer?.forceContextLoss();
    if(this.cookiesItems.length) {
      this.cookiesItems.forEach(({ cookie, body }) => {
        body.removeEventListener('collide', playHitSound)
        cookie && cookie.traverse && cookie.traverse(() => {
          obj.material?.dispose();
        })
      })
      this.cookiesItems.length = 0;
    }
    this.scene?.traverse((obj) => {
      if (obj instanceof THREE.Mesh) {
        obj.material?.dispose();
        obj.geometry?.dispose();
      }
    });
  }
  loadCookieModel() {
    this.gltfLoader.load(ck, (gltf) => {
      this.model = gltf.scene;
      this.model.children[0].position.y -= .01
      this.model.children[0].scale.setScalar(.08)
      this.model.rotation.y = 0
      this.model.traverse(mesh => {
        if (mesh.isMesh) {
          mesh.receiveShadow = true;
          mesh.castShadow = true;
        }
      })
    })
  }
  createCookies(position) {
    let cookies = this.model.clone();
    let material = cookies.getObjectByName("Object_2").material.clone()
    material.map = this.textures[Math.floor(Math.random()*this.textures.length)]
    material.map.flipY = false;
    material.map.encoding = THREE.sRGBEncoding
    material.map.wrapS = 1000
    material.map.wrapT = 1000
    material.needsUpdate = true;
    cookies.getObjectByName("Object_2").material = material
    cookies.position.set(0, .15, 0)
    let body = new CANNON.Body({
      mass: 0.2,
      material: new CANNON.Material({
        friction: .15,
        restitution: .01,
      })
    });
    const boxShape = new CANNON.Cylinder(.08, .08, .026, 32)
    body.addShape(boxShape);
    body.position.set(0, 0.2, 0);
    if (position) {
      body.position.copy(position)
    }
    this.cookiesItems.push({
      cookies,
      body
    })
    this.world.addBody(body);
    this.scene.add(cookies);

    body.addEventListener('collide', playHitSound)
  }
  play() {
    if (this.isPlaying) return;
    this.isPlaying = true;
    this.render();
  }
  stop() {
    this.isPlaying = false;
  }
  render() {
    if (!this.isPlaying) return;
    this.clock = new THREE.Clock()
    window.requestAnimationFrame(() => {
      this.update();
      this.render();
    });
  }
  update() {
    const { scene, camera, renderer } = this;
    let dt = this.clock.getDelta()
    this.updatePhysics(dt)
    renderer && renderer.render(scene, camera);
  }
}

export default Sketch;
